Socket
Socket
Sign inDemoInstall

perfect-freehand

Package Overview
Dependencies
14
Maintainers
1
Versions
56
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    perfect-freehand

Draw perfect pressure-sensitive freehand strokes.


Version published
Weekly downloads
53K
decreased by-0.41%
Maintainers
1
Created
Weekly downloads
 

Readme

Source

Screenshot

Draw perfect pressure-sensitive freehand strokes.

🔗 Try out a demo.

💰 Using this library in a commercial product? Consider becoming a sponsor.

Table of Contents

Installation

npm install perfect-freehand

or

yarn add perfect-freehand

Usage

This package's default export is a function that:

  • accepts an array of points and an (optional) options object
  • returns a stroke outline as an array of points formatted as [x, y]
import getStroke from 'perfect-freehand'

You may format your input points as array or an object. In both cases, the value for pressure is optional (it will default to .5).

getStroke([
  [0, 0, 0],
  [10, 5, 0.5],
  [20, 8, 0.3],
])

getStroke([
  { x: 0, y: 0, pressure: 0 },
  { x: 10, y: 5, pressure: 0.5 },
  { x: 20, y: 8, pressure: 0.3 },
])

Options

The options object is optional, as are each of its properties.

PropertyTypeDefaultDescription
sizenumber8The base size (diameter) of the stroke.
thinningnumber.5The effect of pressure on the stroke's size.
smoothingnumber.5How much to soften the stroke's edges.
streamlinenumber.5How much to streamline the stroke.
simulatePressurebooleantrueWhether to simulate pressure based on velocity.
easingfunctiont => tAn easing function to apply to each point's pressure.
start{ }Tapering options for the start of the line.
end{ }Tapering options for the end of the line.
lastbooleantrueWhether the stroke is complete.

Note: When the last property is true, the line's end will be drawn at the last input point, rather than slightly behind it.

The start and end options accept an object:

PropertyTypeDefaultDescription
capbooleantrueWhether to draw a cap.
tapernumber0The distance to taper.
easingfunctiont => tAn easing function for the tapering effect.

Note: The cap property has no effect when taper is more than zero.

getStroke(myPoints, {
  size: 8,
  thinning: 0.5,
  smoothing: 0.5,
  streamline: 0.5,
  easing: (t) => t * t * t,
  simulatePressure: true,
  last: true,
  start: {
    taper: 20,
    easing: (t) => t * t * t,
  },
  end: {
    taper: 20,
    easing: (t) => t * t * t,
  },
})

Tip: To create a stroke with a steady line, set the thinning option to 0.

Tip: To create a stroke that gets thinner with pressure instead of thicker, use a negative number for the thinning option.

Rendering

While getStroke returns an array of points representing the outline of a stroke, it's up to you to decide how you will render these points.

The function below will turn the points returned by getStroke into SVG path data.

function getSvgPathFromStroke(stroke) {
  if (!stroke.length) return ''

  const d = stroke.reduce(
    (acc, [x0, y0], i, arr) => {
      const [x1, y1] = arr[(i + 1) % arr.length]
      acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
      return acc
    },
    ['M', ...stroke[0], 'Q']
  )

  d.push('Z')
  return d.join(' ')
}

To use this function, first use perfect-freehand to turn your input points into a stroke outline, then pass the result to getSvgPathFromStroke.

import getStroke from 'perfect-freehand'

const myStroke = getStroke(myInputPoints)

const pathData = getSvgPathFromStroke(myStroke)

You could then pass this string either to an SVG path element:

<path d={pathData} />

Or, if you are rendering with HTML Canvas, you can pass the result to a Path2D constructor).

const myPath = new Path2D(pathData)
ctx.fill(myPath)

Flattening

To render a stroke as a "flattened" polygon, add the polygon-clipping package and use the following function together with the getSvgPathFromStroke.

import polygonClipping from 'polygon-clipping'

function getFlatSvgPathFromStroke(stroke) {
  const poly = polygonClipping.union([stroke])

  const d = []

  for (let face of poly) {
    for (let points of face) {
      d.push(getSvgPathFromStroke(points))
    }
  }

  return d.join(' ')
}

Tip: For implementations in Typescript, see the example project included in this repository.

Example

import * as React from 'react'
import getStroke from 'perfect-freehand'
import { getSvgPathFromStroke } from './utils'

export default function Example() {
  const [points, setPoints] = React.useState()

  function handlePointerDown(e) {
    e.preventDefault()
    setPoints([[e.pageX, e.pageY, e.pressure]])
  }

  function handlePointerMove(e) {
    if (e.buttons === 1) {
      e.preventDefault()
      setPoints([...points, [e.pageX, e.pageY, e.pressure]])
    }
  }

  return (
    <svg
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      style={{ touchAction: 'none' }}
    >
      {points && (
        <path
          d={getSvgPathFromStroke(
            getStroke(points, {
              size: 24,
              thinning: 0.5,
              smoothing: 0.5,
              streamline: 0.5,
            })
          )}
        />
      )}
    </svg>
  )
}

Edit perfect-freehand-example

Advanced Usage

StrokeOptions

A TypeScript type for the options object.

import { StrokeOptions } from 'perfect-freehand'

For advanced usage, the library also exports smaller functions that getStroke uses to generate its SVG data. While you can use getStroke's data to render strokes with an HTML canvas (via the Path2D element) or with SVG paths, these new functions will allow you to create paths in other rendering technologies.

getStrokePoints
import { strokePoints } from 'perfect-freehand'
const strokePoints = getStrokePoints(rawInputPoints)

Accepts an array of points (formatted either as [x, y, pressure] or { x: number, y: number, pressure: number}) and a streamline value. Returns a set of streamlined points as [x, y, pressure, angle, distance, lengthAtPoint]. The path's total length will be the length of the last point in the array.

getOutlinePoints

Accepts an array of points (formatted as [x, y, pressure, angle, distance, length], i.e. the output of getStrokePoints) and returns an array of points ([x, y]) defining the outline of a pressure-sensitive stroke.

import { getOutlinePoints } from 'perfect-freehand'
const outlinePoints = getOutlinePoints(strokePoints)

Support

Please open an issue for support.

Discussion

Have an idea or casual question? Visit the discussion page.

Author

Keywords

FAQs

Last updated on 15 Sep 2021

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc